package at.fhj.antesk.steamvaluecalc.implementations;

import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import at.fhj.antesk.steamvaluecalc.data.Game;
import at.fhj.antesk.steamvaluecalc.interfaces.DataHandler;

public class DataHandlerImpl implements DataHandler {

    private static final String GAMES_NODE = "games";
    private static final String ID_NODE = "appID";
    private static final String GAME_NODE = "game";
    private static final String TEXT_NODE_IGNORE = "#text";
    private static final String NAME_NODE = "name";
    private static final String LOGO_URL_NODE = "logo";
    private static final String STORE_LINK_NODE = "storeLink";
    private static final String HOURS_ON_RECORD_NODE = "hoursOnRecord";

    private static Logger logger = LoggerFactory.getLogger(DataHandlerImpl.class);

    private String outputFilePath;
    private String inputFilePath;

    public DataHandlerImpl(String input, String output) {
        inputFilePath = input;
        outputFilePath = output;
    }

    public synchronized void addGame(Game gameToStore) {
        addGameToFile(gameToStore, outputFilePath);
    }

    public synchronized void addGameToFile(Game gameToStore, String filePath) {
        // TODO syncronization should be tested and checked as we call not
        // syncronized blocks from here
        /**
         * It could be a good idea to return the games as a hashmap instead of a
         * list with keys as gameids. This way we could look up for a game
         * quickly. Also it would be sure that only 1 game with the given id
         * exits in the map.
         */
        List<Game> games = loadGamesFromFile(filePath);
        if (!isGameAlreadyStored(games, gameToStore)) {
            games.add(gameToStore);
        }
        storeGamesToFile(games, filePath);
    }

    private boolean isGameAlreadyStored(List<Game> games, Game gameToStore) {
        boolean isGameNew = false;
        for (Game game : games) {
            if (game.getId().equals(gameToStore.getId())) {
                isGameNew = true;
                break;
            }
        }

        return isGameNew;
    }

    public void storeGames(List<Game> games) {
        storeGamesToFile(games, outputFilePath);

    }

    public void storeGamesToFile(List<Game> games, String filePath) {
        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = documentBuilder.newDocument();
            Element gamesNode = document.createElement(GAMES_NODE);
            document.appendChild(gamesNode);

            for (Game game : games) {
                Element gameNode = document.createElement(GAME_NODE);
                Element id = document.createElement(ID_NODE);
                id.setTextContent(game.getId());
                gameNode.appendChild(id);
                Element name = document.createElement(NAME_NODE);
                name.setTextContent(game.getName());
                gameNode.appendChild(name);
                Element logo = document.createElement(LOGO_URL_NODE);
                logo.setTextContent(game.getLogo());
                gameNode.appendChild(logo);
                Element storeLink = document.createElement(STORE_LINK_NODE);
                storeLink.setTextContent(game.getStoreLink());
                gameNode.appendChild(storeLink);
                Element hoursOnRecord = document.createElement(HOURS_ON_RECORD_NODE);
                hoursOnRecord.setTextContent(game.getHoursOnRecord().toString());
                gameNode.appendChild(hoursOnRecord);
                // TODO add missing fields such as price with null checks
                gamesNode.appendChild(gameNode);
            }
            DOMSource source = new DOMSource(document);
            PrintStream ps = new PrintStream(filePath);
            StreamResult result = new StreamResult(ps);

            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.transform(source, result);
        } catch (ParserConfigurationException e) {
            logger.error("Parsing Error.", e);
        } catch (IOException e) {
            logger.error("IO Error.", e);
        } catch (TransformerConfigurationException e) {
            logger.error("Transformer Configuration Error.", e);
        } catch (TransformerException e) {
            logger.error("Transformer Error.", e);
        }

    }

    public List<Game> loadGames() {
        return loadGamesFromFile(inputFilePath);
    }

    public List<Game> loadGamesFromFile(String filePath) {
        List<Game> gameList = new ArrayList<Game>();

        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder;
            documentBuilder = dbf.newDocumentBuilder();
            Document gamesFile = documentBuilder.parse(filePath);

            NodeList gameElements = gamesFile.getElementsByTagName(GAME_NODE);
            for (int i = 0; i < gameElements.getLength(); i++) {
                Node gameItem = gameElements.item(i);

                NodeList gameItemAttributes = gameItem.getChildNodes();
                Game game = getGame(gameItemAttributes);
                gameList.add(game);
            }
        } catch (ParserConfigurationException e) {
            logger.error("Parsing Error.", e);
        } catch (SAXException e) {
            logger.error("SAX Error.", e);
        } catch (IOException e) {
            logger.error("IO Error.", e);
        }

        return gameList;
    }

    private Game extractGameData(NodeList gameItemAttributes, Game game) {

        for (int j = 0; j < gameItemAttributes.getLength(); j++) {
            Node attribute = gameItemAttributes.item(j);
            if (!attribute.getTextContent().equals(TEXT_NODE_IGNORE)) {
                if (attribute.getNodeName().equals(NAME_NODE)) {
                    game.setName(attribute.getTextContent());
                    continue;
                }
                if (attribute.getNodeName().equals(LOGO_URL_NODE)) {
                    game.setLogo(attribute.getTextContent());
                    continue;
                }
                if (attribute.getNodeName().equals(STORE_LINK_NODE)) {
                    game.setStoreLink(attribute.getTextContent());
                    continue;
                }
                if (attribute.getNodeName().equals(HOURS_ON_RECORD_NODE)) {
                    game.setHoursOnRecord(Double.parseDouble(attribute.getTextContent()));
                    continue;
                }
            }
        }
        return game;
    }

    private Game getGame(NodeList gameItemAttributes) {
        Game game = null;
        for (int j = 0; j < gameItemAttributes.getLength(); j++) {
            Node attribute = gameItemAttributes.item(j);
            if (!attribute.getTextContent().equals(TEXT_NODE_IGNORE)) {
                if (attribute.getNodeName().equals(ID_NODE)) {
                    String getGameId = attribute.getTextContent();
                    game = new Game(getGameId);
                    break;
                }
            }
        }
        game = extractGameData(gameItemAttributes, game);
        return game;
    }

    public void updateGame(Game game) {
        updateGameInFile(game);
    }

    public void updateGameInFile(Game game) {
        List<Game> games = loadGames();
        int gameIndex = games.indexOf(game);
        if (-1 != gameIndex) {
            games.set(gameIndex, game);
            storeGames(games);
        }

    }

    public void updateGamesInFile(List<Game> gamesToUpdate) {
        List<Game> games = loadGames();
        boolean isUpdate = false;
        for (Game game : gamesToUpdate) {
            int gameIndex = games.indexOf(game);
            if (-1 != gameIndex) {
                isUpdate = true;
                games.set(gameIndex, game);
            }
        }
        if (isUpdate) {
            storeGames(games);
        }

    }

}
